package my.georings.service;

import java.util.Hashtable;
import java.util.Iterator;
import java.util.Timer;
import java.util.TimerTask;
import java.util.concurrent.ArrayBlockingQueue;

import my.Utils;
import my.georings.ApplicationStateListener;
import my.georings.GeoPhoneRingsApp;
import my.georings.data.MyLocation;
import my.georings.data.MyLocationsManager;
import my.georings.data.MyLocationsManagerListener;
import my.georings.data.Settings;
import android.app.Service;
import android.content.Intent;
import android.location.Criteria;
import android.location.Location;
import android.location.LocationListener;
import android.location.LocationManager;
import android.os.Binder;
import android.os.Bundle;
import android.os.Handler;
import android.os.IBinder;
import android.util.Log;
import android.widget.Toast;

public class LocationChangeService extends Service implements LocationListener,
		ApplicationStateListener, MyLocationsManagerListener {

	public static final boolean TEST = true;

	private Timer timer = new Timer();
	private Timer timer2 = null;
	private int timerToDelete = 2;

	private Settings sets = new Settings();
	private LocationManager locationManager;
	
	private static final long MINIMUM_DISTANCE_CHANGE_FOR_UPDATES = 0; // in
																		// Meters
	private static final long MINIMUM_TIME_BETWEEN_UPDATES = 0;
	private static final int MAX_LOCATION_UPDATES = 1;

	private Adjusting adjustingObject = new Adjusting();

	private boolean appIsOn = true;
	private static final int UPDATE_APP_IS_ON = TEST ? 5 * 1000 : 30 * 1000;

	private boolean locationUpdates = false;
	private int locationsUpdatesCounter = 0;

	private boolean settingsIntervalChanged = false;

	private Handler handler = new Handler();
	private Runnable r;

	private Location lastLocation;

	private ArrayBlockingQueue<MyLocationChangedListener> listeners = new ArrayBlockingQueue<MyLocationChangedListener>(
			5);

	private MyLocationsManager myLocationsManager;

	private MyLocation currentLocation = null;

	private MyLocation defaultOne = new MyLocation();

	private boolean firstTime = true;

	@Override
	public void onCreate() {
		super.onCreate();

		locationManager = (LocationManager) getSystemService(LOCATION_SERVICE);
		// Log.d("GeoPhoneRings", "service started");

		sets.copyFrom(((GeoPhoneRingsApp) getApplication()).getSettings());
		myLocationsManager = ((GeoPhoneRingsApp) getApplication())
				.getMyLocationsManager();

		lastLocation = locationManager
				.getLastKnownLocation(LocationManager.NETWORK_PROVIDER);
		if (lastLocation == null)
			lastLocation = locationManager
					.getLastKnownLocation(LocationManager.GPS_PROVIDER);

		defaultOne = SettingsChanger.getDefaultRingSettings(this);
		startService();
	}

	@Override
	public void onDestroy() {
		stopService();
		// Log.d("GeoPhoneRings", "service destroyed");
		super.onDestroy();
	}

	/*********************** START AND STOP ******************/

	/**
	 * Starts the service.
	 */
	private void startService() {
		appIsOn = true;
		firstTime = true;

		((GeoPhoneRingsApp) getApplication()).getMyLocationsManager()
				.setManagerListener(this);

		timer.schedule(new TimerTask() {
			@Override
			public void run() {
				main();
			}
		}, 100, getNextUpdateTime());
	}

	/**
	 * Stops the service.
	 */
	private void stopService() {
		if (timer != null)
			timer.cancel();
		if (timer2 != null)
			timer2.cancel();

		// change all settings to default one
		SettingsChanger.changeSettingsAccordingToLocation(defaultOne, this);
		if (appIsOn) {
			((GeoPhoneRingsApp) getApplication()).getMyLocationsManager()
					.setManagerListener(null);
		}
	}

	/********************** CHANGE SETTINGS *************************/

	/**
	 * Change interval between location updates.
	 */
	public void changeInterval(int newOne) {
		sets.setInterval(newOne);
		settingsIntervalChanged = true;
	}

	/**
	 * Change using gps
	 * 
	 * @param gps
	 *            on/off gps usage
	 */
	public void changeUseGPS(boolean gps) {
		sets.setGps(gps);
	}

	/**
	 * Whether service should adjust interval between updates according to
	 * distance from known locations.
	 * 
	 * @param adjust
	 *            on/off adjusting
	 */
	public void changeAdjust(boolean adjust) {
		sets.setReduceInterval(adjust);
	}

	/********************** BINDER *****************/

	private final IBinder binder = new LocationChangeBinder();

	public class LocationChangeBinder extends Binder {
		public LocationChangeService getService() {
			return LocationChangeService.this;
		}
	}

	@Override
	public IBinder onBind(Intent arg0) {
		return binder;
	}

	/*********************** MAIN LOOP ***********************/

	/**
	 * Main loop of service. Called every specified interval. In every loop: -
	 * if update interval has changed timers are switched - there are 3 location
	 * updates
	 */
	private void main() {

		if (locationUpdates == false) {

			handler.post(r = new Runnable() {
				@Override
				public void run() {
					startLocationUpdates();
				}
			});

			// Log.d("..............GeoPhoneRings...............", "APP IS ON: "
			// + appIsOn);

			if (timer != null && timerToDelete == 1) {
				timer = null;
			} else if (timer2 != null && timerToDelete == 2) {
				timer2 = null;
			}

			// jesli sa nowe ustawienia intervalu
			if (settingsIntervalChanged) {
				// Log.d("GeoPhoneRings", "reschedule to " +
				// getNextUpdateTime()/1000 + "s");

				settingsIntervalChanged = false;

				if (timer == null) {
					timer = new Timer();
					timer.schedule(new TimerTask() {
						@Override
						public void run() {
							main();
						}
					}, 0, getNextUpdateTime());

					timer2.cancel();
					timerToDelete = 2;

				} else if (timer2 == null) {
					timer2 = new Timer();
					timer2.schedule(new TimerTask() {
						@Override
						public void run() {
							main();
						}
					}, 0, getNextUpdateTime());

					timer.cancel();
					timerToDelete = 1;
				}
			}
		}
	}

	/******************** LOCATION UPDATES **********************/

	private void startLocationUpdates() {
		locationUpdates = true;

		// Log.d("GeoPhoneRings", "start updates");

		String provider = null;

		// ONLY FOR TEST
		if (TEST) {
			provider = sets.isGps()
					&& locationManager
							.isProviderEnabled(LocationManager.GPS_PROVIDER) ? LocationManager.GPS_PROVIDER
					: null;
		} else {

			if (sets.isGps()
					&& locationManager
							.isProviderEnabled(LocationManager.GPS_PROVIDER)) {
				Criteria criteria = new Criteria();
				criteria.setCostAllowed(false);
				criteria.setPowerRequirement(Criteria.POWER_LOW);
				criteria.setSpeedRequired(false);
				criteria.setAccuracy(Criteria.ACCURACY_COARSE);
				provider = locationManager.getBestProvider(criteria, true);
			} else {
				provider = locationManager
						.isProviderEnabled(LocationManager.NETWORK_PROVIDER) ? LocationManager.NETWORK_PROVIDER
						: null;
			}
		}

		// if there is no provider
		if (provider == null) {
			notifyListenersNoProvider();
			if (appIsOn) {
				((GeoPhoneRingsApp) getApplication()).setRestart(true);
			}
			stopService();
		} else {

			locationManager.requestLocationUpdates(provider,
					MINIMUM_TIME_BETWEEN_UPDATES,
					MINIMUM_DISTANCE_CHANGE_FOR_UPDATES, this);
		}
	}

	private void stopLocationUpdates() {

		// Log.d("GeoPhoneRings", "stop updates");
		handler.removeCallbacks(r);

		locationsUpdatesCounter = 0;
		locationUpdates = false;
		locationManager.removeUpdates(this);
	}

	@Override
	public void onLocationChanged(Location loc) {
		++locationsUpdatesCounter;

		// get the location
		 Log.d("GeoPhoneRings", "loop: " + locationsUpdatesCounter + " " +
		 loc.getLatitude() + " " + loc.getLongitude());

		if (firstTime || locationsUpdatesCounter >= MAX_LOCATION_UPDATES) {

			lastLocation = loc;
			
			if(TEST){
				lastLocation.setAccuracy(50.0f);
			}

			// do all the magic :)
			onLocationChanged();
			stopLocationUpdates();
			firstTime = false;
		}
	}

	@Override
	public void onProviderDisabled(String provider) {
	}

	@Override
	public void onProviderEnabled(String provider) {
	}

	@Override
	public void onStatusChanged(String provider, int status, Bundle extras) {
	}

	// gps location changed significally
	synchronized private void onLocationChanged() {
		// Log.d(this.toString(), "location changed");

		// check out the nearest known location
		Hashtable<Float, MyLocation> nearest = myLocationsManager.getNearestLocation(lastLocation);

		// there is nearest location
		if (nearest != null) {
			float distance = ((Float) nearest.keySet().toArray()[0]).floatValue()
					- lastLocation.getAccuracy();
			boolean smthChanged = false;

			MyLocation nextLocation = (MyLocation) nearest.values().toArray()[0];
			if (distance <= 0) {
				// change only when location trully changed
				

				// if there is no current location or current location is
				// different than nearest one to change
				if (currentLocation == null
						|| nextLocation.getId() != currentLocation.getId()) {

					currentLocation = nextLocation;
					knownLocationChanged();
					smthChanged = true;
				}
				// the same location but settings are different
				else if (currentLocation.getId() == nextLocation.getId()
						&& !currentLocation.equals(nextLocation)) {
					currentLocation = nextLocation;
					currentKnownLocationChanged();
					smthChanged = true;
				}
			}
			// distance is bigger, if there is current location - it should be
			// changed to default
			else if (currentLocation != null) {
				knownLocationEnded();
				smthChanged = true;
			}

			// check adjusting, cause there are some locations
			if (!appIsOn && smthChanged && sets.isReduceInterval()
					&& adjustingObject.adjustFromDistance(distance)) {
				settingsIntervalChanged = true;
			}
		}
		// there is no nearest location - so there is no locations added
		else {
			// if there was any location current - it definietely ended, because
			// there in none right now
			if (currentLocation != null) {
				knownLocationEnded();
			}
		}

		notifyListeners();
	}

	// known user location changed
	private void knownLocationChanged() {
		SettingsChanger
				.changeSettingsAccordingToLocation(currentLocation, this);
		Utils.showToast(this, "Your location is now: "
				+ currentLocation.getName(), Toast.LENGTH_LONG);
	}

	private void currentKnownLocationChanged() {
		SettingsChanger
				.changeSettingsAccordingToLocation(currentLocation, this);
		Utils.showToast(this, "Your location settings changed: "
				+ currentLocation.getName(), Toast.LENGTH_LONG);
	}

	// go back to previous settings
	private void knownLocationEnded() {
		String currName = currentLocation.getName();
		currentLocation = null;
		SettingsChanger.changeSettingsAccordingToLocation(defaultOne, this);
		Utils.showToast(this, "Your location has ended: " + currName,
				Toast.LENGTH_LONG);
	}

	private void notifyListeners() {
		Iterator<MyLocationChangedListener> iter = listeners.iterator();
		while (iter.hasNext()) {
			iter.next().onMyLocationChanged(lastLocation);
		}
	}

	private void notifyListenersNoProvider() {
		Iterator<MyLocationChangedListener> iter = listeners.iterator();
		while (iter.hasNext()) {
			iter.next().onNoProviderAvailable();
		}
	}

	public void addListener(MyLocationChangedListener l) {
		listeners.add(l);
	}

	public void removeListener(MyLocationChangedListener l) {
		listeners.remove(l);
	}

	public Location getLastLocation() {
		return lastLocation;
	}

	public Settings getSets() {
		return sets;
	}

	public MyLocationsManager getMyLocationsManager() {
		return myLocationsManager;
	}

	public void setMyLocationsManager(MyLocationsManager myLocationsManager) {
		this.myLocationsManager = myLocationsManager;
	}

	@Override
	public void onApllicationOn(boolean isOn) {
		appIsOn = isOn;
		settingsIntervalChanged = true;
	}

	private int getNextUpdateTime() {
		if (appIsOn)
			return UPDATE_APP_IS_ON;

		if (sets.isReduceInterval() && adjustingObject.isAdjusting())
			return TEST ? Adjusting.UPDATE_ADJUST/5 : Adjusting.UPDATE_ADJUST;

		return TEST ? sets.getInterval()/10 : sets.getInterval();
	}

	@Override
	public void onLocationAdded(MyLocation location) {
		onLocationChanged();
	}

	@Override
	public void onLocationChanged(MyLocation location) {
		// current location settings changed
		if (currentLocation != null
				&& (currentLocation.getId() == location.getId())) {
			currentLocation = location;
			currentKnownLocationChanged();
		}
	}

	@Override
	public void onLocationDeleted(MyLocation location) {
		// current location id removed
		if (currentLocation != null
				&& currentLocation.getId() == location.getId()) {
			knownLocationEnded();
		}
	}

	@Override
	public void onLocationOnOff(MyLocation location) {
		// if this is current location - turn it off and check another
		if (currentLocation != null
				&& location.getId() == currentLocation.getId()
				&& !location.isON()) {
			knownLocationEnded();
			onLocationChanged();
		}
		// there is current location but maybe this one that is turned on will
		// be closer
		else {
			onLocationChanged();
		}
	}

}
